package com.thoughtworks.excelparser;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.poi.ss.usermodel.Sheet;

import com.thoughtworks.excelparser.annotations.ExcelField;
import com.thoughtworks.excelparser.annotations.ExcelObject;
import com.thoughtworks.excelparser.annotations.MappedExcelObject;
import com.thoughtworks.excelparser.annotations.ParseType;
import com.thoughtworks.excelparser.exception.ExcelParsingException;
import com.thoughtworks.excelparser.helper.HSSFHelper;

public class SheetParser {
	private HSSFHelper hssfHelper;
	private Map<String, Map<Integer, Field>> excelMapCache;
	private static final Logger LOGGER = Logger.getLogger(SheetParser.class);

	public SheetParser() {
		hssfHelper = new HSSFHelper();
		excelMapCache = new HashMap<String, Map<Integer, Field>>();
	}

	public <T> List<T> createEntity(Sheet sheet, String sheetName, Class<T> clazz)
	        throws ExcelParsingException {
		List<T> list = new ArrayList<T>();
		ExcelObject excelObject = getExcelObject(clazz);
		for (int currentLocation = excelObject.start(); currentLocation <= excelObject.end(); currentLocation++) {
			T object = getNewInstance(sheet, sheetName, clazz, excelObject.parseType(), currentLocation, excelObject.zeroIfNull());
			List<Field> mappedExcelFields = getMappedExcelObjects(clazz);
			for (Field mappedField : mappedExcelFields) {
				Class<?> fieldType = mappedField.getType();
				List<?> fieldValue = createEntity(sheet, sheetName, fieldType.equals(List.class) ? getFieldType(mappedField)
				        : fieldType);
				if (fieldType.equals(List.class)) {
					setFieldValue(mappedField, object, fieldValue);
				} else if (!fieldValue.isEmpty()) {
					setFieldValue(mappedField, object, fieldValue.get(0));
				}
			}
			list.add(object);
		}
		return list;
	}

	private Class<?> getFieldType(Field field) {
		Type type = field.getGenericType();
		if (type instanceof ParameterizedType) {
			ParameterizedType pt = (ParameterizedType) type;		
			return (Class<?>) pt.getActualTypeArguments()[0];
		}

		return null;
	}

	private <T> List<Field> getMappedExcelObjects(Class<T> clazz) {
		List<Field> fieldList = new ArrayList<Field>();
		Field[] fields = clazz.getDeclaredFields();
		for (Field field : fields) {
			MappedExcelObject mappedExcelObject = field.getAnnotation(MappedExcelObject.class);
			if (mappedExcelObject != null) {
				field.setAccessible(true);
				fieldList.add(field);
			}
		}
		return fieldList;
	}

	private <T> ExcelObject getExcelObject(Class<T> clazz) throws ExcelParsingException {
		ExcelObject excelObject = clazz.getAnnotation(ExcelObject.class);
		if (excelObject == null) {
			throw new ExcelParsingException("Invalid class configuration - ExcelObject annotation missing - "
			        + clazz.getSimpleName());
		}
		return excelObject;
	}

	private <T> T getNewInstance(Sheet sheet, String sheetName, Class<T> clazz, ParseType parseType,
	        Integer currentLocation, boolean zeroIfNull) throws ExcelParsingException {
		T object = getInstance(clazz);
		Map<Integer, Field> excelPositionMap = getExcelFieldPositionMap(clazz);
		for (Integer position : excelPositionMap.keySet()) {
			Field field = excelPositionMap.get(position);
			Object cellValue;
			if (ParseType.ROW == parseType) {
				cellValue = hssfHelper.getCellValue(sheet, sheetName, field.getType(), currentLocation, position,
				        zeroIfNull);
			} else {
				cellValue = hssfHelper.getCellValue(sheet, sheetName, field.getType(), position, currentLocation,
				        zeroIfNull);
			}
			setFieldValue(field, object, cellValue);
		}

		return object;
	}

	private <T> T getInstance(Class<T> clazz) throws ExcelParsingException {
		T object = null;
		try {
			object = clazz.newInstance();
		} catch (Exception e) {
			LOGGER.error(e);
			throw new ExcelParsingException("Exception occured while instantiating the class " + clazz.getName(), e);
		}
		return object;
	}

	private <T> void setFieldValue(Field field, T object, Object cellValue) throws ExcelParsingException {
		try {
            InputStream is = this.getClass().getResourceAsStream("/excelValueReplace.properties");
            Properties pro = new Properties();
            pro.load(is);
            String property = null;
            if(cellValue!=null){
                 property = pro.getProperty(cellValue.toString());
            }
            if(StringUtils.isNotBlank(property)){
                cellValue = property;
            }
			field.set(object, cellValue);
		} catch (IllegalArgumentException e) {
			LOGGER.error(e.getMessage(), e);
			throw new ExcelParsingException("Exception occured while setting field value ", e);
		} catch (IllegalAccessException e) {
			LOGGER.error(e.getMessage(), e);
			throw new ExcelParsingException("Exception occured while setting field value ", e);
		} catch (IOException e) {
            e.printStackTrace();
        }
    }

	private <T> Map<Integer, Field> getExcelFieldPositionMap(Class<T> clazz) {
		Map<Integer, Field> existingMap = excelMapCache.get(clazz.getName());
		return existingMap == null ? loadCache(clazz) : existingMap;
	}

	/**
	 * Load cached for the given class.
	 * 
	 * @param clazz
	 *            Class object to investigate.
	 * @return Map.
	 */
	private <T> Map<Integer, Field> loadCache(Class<T> clazz) {
		Map<Integer, Field> fieldMap = new HashMap<Integer, Field>();
		Field[] fields = clazz.getDeclaredFields();
		for (Field field : fields) {
			ExcelField excelField = field.getAnnotation(ExcelField.class);
			if (excelField != null) {
				field.setAccessible(true);
				fieldMap.put(excelField.position(), field);
			}
		}
		excelMapCache.put(clazz.getName(), fieldMap);
		return fieldMap;
	}
}
